feat(api): track unread thread count - #978
Conversation
Replace the boolean read state with an unread message counter so repeated inbound activity remains visible until the thread is explicitly cleared. BREAKING CHANGE: Message thread responses and updates use unread_count instead of is_read. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8efee0ef-a8be-4d9c-b4ec-33dcbf0dfdfd
Up to standards ✅🟢 Issues
|
| Metric | Results |
|---|---|
| Duplication | -2 |
NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.
Greptile SummaryThis PR replaces the message-thread read boolean with a persisted unread counter and updates inbound-event handling, the API contract, dashboard state, documentation, and tests.
Confidence Score: 3/5This PR should not merge until delayed inbound events can no longer restore stale unread state and the missed-call preview contract is made consistent. Removing the read watermark permits an older queued event to mark an already-read thread unread again, and the changed missed-call literal causes existing unit and integration assertions to fail. Files Needing Attention: api/pkg/repositories/gorm_message_thread_repository.go, api/pkg/listeners/message_thread_listener.go Important Files Changed
Sequence DiagramsequenceDiagram
participant P as Phone/API
participant Q as Event queue
participant T as Thread listener
participant DB as Thread repository
participant W as Web dashboard
P->>Q: Inbound message event
Q-->>T: Asynchronous delivery
T->>DB: "unread_count = unread_count + 1"
W->>DB: "PUT unread_count = 0"
DB-->>W: Updated thread
Reviews (1): Last reviewed commit: "feat(api)!: track unread thread count" | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR updates httpSMS message threads to track an integer unread_count instead of a boolean is_read, aligning backend persistence, API contracts/docs, web UI state, and tests to support incrementing unread counts on inbound activity and clearing them via thread updates.
Changes:
- Replace thread read state (
is_read) withunread_countacross API entities, request payloads, repository updates, and generated Swagger docs. - Update web thread stores/pages/components to display unread counts and clear them via
unread_count: 0updates. - Update unit + integration tests and test documentation to validate unread-count behavior.
Reviewed changes
Copilot reviewed 22 out of 23 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| web/shared/types/api.ts | Updates generated TS API types to remove is_read and add unread_count. |
| web/app/stores/threads.ts | Renames “mark read” behavior to clearing unread_count and updates update payload accordingly. |
| web/app/stores/messages.ts | Adds getMessage() helper used by thread page websocket handling. |
| web/app/pages/threads/[id]/index.vue | Updates thread page to clear unread count and attempts to handle inbound websocket events using message_id. |
| web/app/components/MessageThread.vue | Updates thread list UI to show unread badge and bold styling based on unread_count. |
| tests/README.md | Updates E2E checklist item wording to “unread count”. |
| tests/read_receipts_test.go | Renames/updates integration test expectations from is_read to unread_count. |
| api/pkg/validators/message_thread_handler_validator.go | Requires update payload contain is_archived or unread_count. |
| api/pkg/validators/message_thread_handler_validator_test.go | Updates validator test to use UnreadCount field. |
| api/pkg/services/message_thread_service.go | Replaces read-state updates with UnreadCount updates; initializes new threads with unread count. |
| api/pkg/services/message_thread_service_test.go | Updates service tests to validate unread-count behavior (no increment on outbound, etc.). |
| api/pkg/requests/message_thread_update_request.go | Swaps request payload field from is_read to unread_count. |
| api/pkg/requests/message_thread_update_request_test.go | Updates request→params mapping test for UnreadCount. |
| api/pkg/repositories/message_thread_repository.go | Replaces status update struct fields to carry UnreadCount instead of read timestamps. |
| api/pkg/repositories/gorm_message_thread_repository.go | Implements unread-count increment/update logic in GORM updates and thread store upsert. |
| api/pkg/repositories/gorm_message_thread_repository_test.go | Updates repository tests for unread-count update expressions and conflict behavior. |
| api/pkg/listeners/message_thread_listener.go | Adjusts missed-call thread preview content (and still marks thread unread via activity). |
| api/pkg/handlers/message_thread_handler_test.go | Updates handler test request payload to {"unread_count":0}. |
| api/pkg/entities/message_thread.go | Replaces IsRead/LastReadAt with persisted UnreadCount. |
| api/pkg/entities/message_thread_test.go | Updates struct-tag/default test for UnreadCount. |
| api/docs/swagger.yaml | Updates OpenAPI definitions to remove is_read and add unread_count. |
| api/docs/swagger.json | Updates generated Swagger JSON accordingly. |
| api/docs/docs.go | Updates generated swagger docs template accordingly. |
Files not reviewed (1)
- api/docs/docs.go: Generated file
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 21 out of 22 changed files in this pull request and generated 1 comment.
Files not reviewed (1)
- api/docs/docs.go: Generated file
Suppressed comments (2)
Previously missed (2) — in code that hasn't changed since the last review.
api/pkg/repositories/gorm_message_thread_repository_test.go:195
- Duplicate
assert.EqualinTestMessageThreadStatusUpdatesArchiveOnlyis redundant and makes the test noisier than necessary.
})
assert.Equal(t, map[string]any{"is_archived": true}, updates)
assert.Equal(t, map[string]any{"is_archived": true}, updates)
assert.NotContains(t, updates, "unread_count")
api/pkg/repositories/gorm_message_thread_repository.go:51
- Unread-count increments in
messageThreadActivityUpdatesare not idempotent: reprocessing the same inbound event can over-incrementunread_countsince it always doesunread_count + 1and no longer usesEventTimestampas a watermark. Consider guarding the increment so re-applying the sameMessageIDdoes not change the count.
updates["is_archived"] = false
}
if params.MarkAsUnread {
updates["unread_count"] = gorm.Expr("unread_count + ?", 1)
}
return updates
| @@ -12,8 +12,7 @@ type MessageThread struct { | |||
| Owner string `json:"owner" example:"+18005550199"` | |||
| Contact string `json:"contact" example:"+18005550100"` | |||
| IsArchived bool `json:"is_archived" example:"false"` | |||
| IsRead bool `json:"is_read" gorm:"not null;default:true" example:"true"` | |||
| LastReadAt time.Time `json:"-" gorm:"not null;default:CURRENT_TIMESTAMP"` | |||
| UnreadCount uint `json:"unread_count" gorm:"not null;default:0" example:"0"` | |||
| UserID UserID `json:"user_id" example:"WB7DRDWrJZRGbYrv2CKGkqbzvqdC"` | |||
There was a problem hiding this comment.
Fixed in 283621e. Added the composite unique index on (user_id, owner, contact), with explicit column priorities matching the ON CONFLICT target, plus regression coverage for the GORM tags.
Create the composite unique index required by the message-thread upsert so fresh databases can execute its ON CONFLICT target. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8efee0ef-a8be-4d9c-b4ec-33dcbf0dfdfd
Summary
is_readstate withunread_countBreaking change
Message thread responses and update payloads now use
unread_countinstead ofis_read.Tests
cd api && go test ./...cd tests && go test -run '^$' ./...Full Docker integration execution was not run because Docker is unavailable in the local environment.